package com.gitee.dictionary.base.impl;

import com.gitee.dictionary.base.BaseDictionaryEnum;
import com.gitee.dictionary.base.DictionaryEnumDao;
import com.gitee.dictionary.cache.DictionaryEnumCache;
import com.gitee.dictionary.exception.DictionaryCodeCheckException;
import com.gitee.dictionary.properties.ConfigProperties;
import com.github.benmanes.caffeine.cache.Cache;
import com.github.benmanes.caffeine.cache.Caffeine;
import org.apache.logging.log4j.util.Strings;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.BeansException;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.lang.NonNull;

import java.util.HashMap;
import java.util.Map;
import java.util.Set;

/**
 * 默认的 DictionaryEnumDao
 *
 * @author <a href="mailto:lin-mt@outlook.com">lin-mt</a>
 */
public class DefaultDictionaryEnumDaoImpl implements DictionaryEnumDao, ApplicationContextAware {
    
    private final Logger logger = LoggerFactory.getLogger(DefaultDictionaryEnumDaoImpl.class);
    
    private final ConfigProperties configProperties;
    
    private ApplicationContext applicationContext;
    
    /**
     * 缓存所有的数据字典，当数据字典多出配置的数量之后则使用Caffeine缓存
     */
    private Cache<String, BaseDictionaryEnum> cache;
    
    private Map<String, BaseDictionaryEnum> cacheAll;
    
    public DefaultDictionaryEnumDaoImpl(ConfigProperties configProperties) {
        this.configProperties = configProperties;
    }
    
    @Override
    public BaseDictionaryEnum getByCode(String code) {
        if (this.cacheAll != null) {
            return this.cacheAll.get(code);
        }
        BaseDictionaryEnum dictionaryEnum = this.cache.getIfPresent(code);
        if (dictionaryEnum == null) {
            dictionaryEnum = this.scanAndGetByCode(code);
            if (dictionaryEnum == null) {
                this.logger.error("未知Code：{}", code);
            } else {
                this.cache.put(code, dictionaryEnum);
            }
        }
        return dictionaryEnum;
    }
    
    @Override
    public void handleDictionaryEnums(@NonNull Set<BaseDictionaryEnum> dictionaryEnums) {
        this.logger.info("共扫描到 {} 个数据字典", dictionaryEnums.size());
        this.checkDictionaryEnums(dictionaryEnums);
        if (dictionaryEnums.size() <= this.configProperties.getCacheSize()
                || this.configProperties.getCacheSize() <= 0) {
            // 系统所有的数据字典数量小于设置的最大缓存数量，则存入ImmutableMap
            this.cacheAll = new HashMap<>(dictionaryEnums.size());
            dictionaryEnums.forEach(dictionaryEnum -> this.cacheAll.put(dictionaryEnum.getCode(), dictionaryEnum));
        } else {
            Caffeine<Object, Object> cacheBuilder = Caffeine.newBuilder();
            this.cache = cacheBuilder.maximumSize(this.configProperties.getCacheSize()).build();
        }
    }
    
    /**
     * 检查数据字典枚举是否有误
     *
     * @param dictionaryEnums 所有的数据字典枚举
     */
    private void checkDictionaryEnums(Set<BaseDictionaryEnum> dictionaryEnums) {
        Map<String, BaseDictionaryEnum> allCode = new HashMap<>();
        for (BaseDictionaryEnum dictionaryEnum : dictionaryEnums) {
            // 检查Code长度是否大于一层的长度
            if (Strings.isBlank(dictionaryEnum.getCode()) || dictionaryEnum.getCode().length() < this.configProperties
                    .getOneLevelLength()) {
                throw new DictionaryCodeCheckException(
                        String.format("%s(%s)：Code长度必须大于 %s", dictionaryEnum, dictionaryEnum.getCode(),
                                this.configProperties.getOneLevelLength()));
            }
            // 检查Code的层数是否为一层长度的整数倍
            if (dictionaryEnum.getCode().length() % this.configProperties.getOneLevelLength() != 0) {
                throw new DictionaryCodeCheckException(
                        String.format("%s(%s)：Code长度必须是 %s 的整数倍", dictionaryEnum, dictionaryEnum.getCode(),
                                this.configProperties.getOneLevelLength()));
            }
            // 检查Code层级是否大于最大值
            if (dictionaryEnum.getCode().length() / this.configProperties.getOneLevelLength() > this.configProperties
                    .getMaxLevelCount()) {
                throw new DictionaryCodeCheckException(
                        String.format("%s(%s)：Code层级不能大于 %s", dictionaryEnum, dictionaryEnum.getCode(),
                                this.configProperties.getMaxLevelCount()));
            }
            // 检查是否存在重复的Code
            if (allCode.containsKey(dictionaryEnum.getCode())) {
                String duplicatedCodeMessage = String
                        .format("%s 与 %s 出现了重复的Code：%s", allCode.get(dictionaryEnum.getCode()), dictionaryEnum,
                                dictionaryEnum.getCode());
                switch (this.configProperties.getDuplicated()) {
                    case EXCEPTION:
                        throw new DictionaryCodeCheckException(duplicatedCodeMessage);
                    case ERROR:
                        this.logger.error(duplicatedCodeMessage);
                        break;
                    case WARNING:
                        this.logger.warn(duplicatedCodeMessage);
                        break;
                    default:
                        // 默认值是抛出异常，不会为null，所以这里不处理
                        break;
                }
            } else {
                allCode.put(dictionaryEnum.getCode(), dictionaryEnum);
            }
        }
    }
    
    /**
     * 扫描，根据 code 获取枚举
     *
     * @param code 数据字典 Code
     * @return 对应的枚举
     */
    public BaseDictionaryEnum scanAndGetByCode(String code) {
        Set<BaseDictionaryEnum> enums = this.applicationContext.getBean(DictionaryEnumCache.class)
                .getAllDictionaryEnumTypes();
        for (BaseDictionaryEnum baseDictionaryEnum : enums) {
            if (code.equals(baseDictionaryEnum.getCode())) {
                return baseDictionaryEnum;
            }
        }
        return null;
    }
    
    @Override
    public void setApplicationContext(@NonNull ApplicationContext applicationContext) throws BeansException {
        this.applicationContext = applicationContext;
    }
}
